﻿/*
 * Cratos Desktop
 * Cratos Cache class
 * 
 * Copyright (c) 2011 Draco
 * 
 * This file is part of Cratos.
 * 
 * Cratos is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * Cratos is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Cratos.  If not, see <http://www.gnu.org/licenses/>.
 * 
 *       .==.        .==.
 *      //'^\\      //^'\\
 *     // ^ ^\(\__/)/^ ^^\\
 *    //^ ^^ ^/6  6\ ^^ ^ \\
 *   //^ ^^ ^/( .. )\^ ^ ^ \\
 *  // ^^ ^/\| V""V|/\^ ^ ^ \\
 * //^^/\/ /  '~~'  \ \/\^ ^ \\
 *  ------------------------------------
 *  HERE BE DRAGONS
 *  
 *  Johan Dercksen - johan@draco.co.za
 */
using System;
using System.IO;
using System.Text;
using System.Linq;
using System.Data;
using System.Timers;
using System.Reflection;
using System.Configuration;
using System.ComponentModel;
using System.Collections.Generic;

using Cratos.Tools.General;
using Cratos.Desktop.Caching;
using Cratos.Tools.DataObject;
using System.Threading.Tasks;
using System.Threading;

namespace Cratos.Desktop
{
	/// <summary>
	/// Cache Task structure
	/// </summary>
	public struct CacheTask
	{
		/// <summary>
		/// Cache Task Name
		/// </summary>
		public string Name { get; set; }

		/// <summary>
		/// Cache Task
		/// </summary>
		public Thread Task { get; set; }

		/// <summary>
		/// Cache Engine
		/// </summary>
		public CratosCacheEngine CacheEngine { get; set; }
	}

	/// <summary>
	/// Cratos Caching class
	/// </summary>
	public class CratosCache : IDisposable
	{
		#region Class Constant declarations
		/// <summary>
		/// Cache Version
		/// </summary>
		public const int CacheVersion = 3;
		#endregion

		#region Class Event declarations
		///// <summary>
		///// Cache Refresh Started
		///// </summary>
		//public event EventHandler CacheRefreshStarted;

		///// <summary>
		///// Cache Refresh Completed
		///// </summary>
		//public event EventHandler CacheRefreshCompleted;
		#endregion

		#region Class Variable declarations
		private static object cacheDBLock = new object();
	
		private bool										m_IsDisposed;
		//private bool										m_IsStartup = true;
		//private BackgroundWorker				m_Worker;
		//private System.Timers.Timer			m_CacheTimer;
		private string									m_CacheFilename					= "CratosClientCache.xml";
		private long										m_CacheExpirySeed				= 1440;		// 24 Hours
		private int											m_CacheRefreshInterval	= 30;			// 30 Seconds

		private List<CacheTask>					allCacheTasks;
		private CancellationTokenSource cacheCancelToken;
		private dsCratosCache						cacheDB;
		private	Configuration						appConfig;
		private	CacheAssemblySection		cacheAssemblySection;
		#endregion

		#region Class Constructors & Destructors
		/// <summary>
		/// Class Constructor
		/// </summary>
		public CratosCache()
		{
			// Create the relevant objects
			this.cacheDB = new dsCratosCache();
		}

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or
		/// resetting unmanaged resources.
		/// </summary>
		public void Dispose() { Dispose(true); GC.SuppressFinalize(this); }

		/// <summary>
		/// Dispose method doing the disposing
		/// </summary>
		/// <param name="isDisposing"></param>
		protected virtual void Dispose(bool isDisposing)
		{
			// Declare the required variables

			// Check to see if Dispose has already been called.
			if (!this.m_IsDisposed)
			{
				// Save the Cache items
				this.SaveCacheDatabase();

				// Dispose the objects if required
				if (isDisposing)
				{
					// Dispose the relevant Connection objects
					if (this.cacheDB != null) { this.cacheDB.Dispose(); this.cacheDB = null; }
				}
			}

			// Set the disposed indicator
			this.m_IsDisposed = true;

			// Return to calling method
			return;
		}
		#endregion

		#region Class Properties
		/// <summary>
		/// Caching Enabled indicator
		/// </summary>
		public bool CachingEnabled { get; set; }

		/// <summary>
		/// Cache Filename
		/// </summary>
		public string CacheFilename
		{
			get { return (this.m_CacheFilename); }
			set { this.m_CacheFilename = value; }
		}

		/// <summary>
		/// Cache Expiry Seed value (Seconds)
		/// </summary>
		public long CacheExpirySeed
		{
			get { return (this.m_CacheExpirySeed); }
			set { this.m_CacheExpirySeed = value; }
		}

		/// <summary>
		/// Cache Refresh Interval (Seconds)
		/// </summary>
		public int CacheRefreshInterval
		{
			get { return (this.m_CacheRefreshInterval); }
			set { this.m_CacheRefreshInterval = value; }
		}

		/// <summary>
		/// Cache DB Lock object
		/// </summary>
		public static object CacheDBLock
		{
			get { return(cacheDBLock); }
			set { cacheDBLock = value; }
		}
		#endregion

		#region Class Methods
		/// <summary>
		/// Start the Cache
		/// </summary>
		/// <returns>A boolean indicating the success of the method</returns>
		public bool StartCache()
		{
			// Declare the required variables
			bool							returnValue = false;
			CacheTask					newCacheTask;
			Assembly					cacheAssembly;
			IEnumerable<Type> cacheAssemblyTypes;

			try
			{
				// Create the relevant object(s)
				this.allCacheTasks    = new List<CacheTask>();
				this.cacheCancelToken = new CancellationTokenSource();

				// Clear the relevant data
				this.cacheDB.Clear();

				// Load the application configuration
				this.appConfig            = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
				this.cacheAssemblySection = (CacheAssemblySection)appConfig.GetSection("CacheAssemblies");

				// Load the Cache Database
				if (this.LoadCacheDatabase())
				{
					#region Cache Assembly Processing
					// Process all the Assemblies in the Cache
					foreach (CacheAssemblyElement currentAssemblyElement in this.cacheAssemblySection.CacheAssemblies)
					{
						// Load the specified Assembly
						cacheAssembly = Assembly.Load(currentAssemblyElement.Assembly);

						// Retrieve the Assembly Types
						cacheAssemblyTypes = from currentType in cacheAssembly.GetTypes()
																 where currentType.IsClass &&
																				(currentType.GetCustomAttributes(typeof(DOCratosCacheableAttribute), true).Count() > 0)
																 select currentType;

						// Process all the classes in the Assembly
						foreach (Type currentType in cacheAssemblyTypes)
						{
							// Add the Type to the Cache
							this.AddCacheDataObject(currentType.AssemblyQualifiedName, currentAssemblyElement);
						}
					}
					#endregion

					// Save the Cache Database
					if (this.SaveCacheDatabase())
					{
						#region Cache Task Creation
						// Start a Task for each of the Data Objects in the Cache DB
						foreach (dsCratosCache.CacheRow currentCacheRow in this.cacheDB.Cache)
						{
							// Create and Setup a new Cache Task structure
							newCacheTask																	 = new CacheTask();
							newCacheTask.Name															 = Type.GetType(currentCacheRow.Cache_DOType).Name;
							newCacheTask.CacheEngine											 = new CratosCacheEngine(this.cacheCancelToken.Token,
																																										 Type.GetType(currentCacheRow.Cache_DOType).Name,
																																										 this.CacheRefreshInterval, 
																																										 this.cacheDB);
							newCacheTask.CacheEngine.CacheDOType           = Type.GetType(currentCacheRow.Cache_DOType);
							newCacheTask.CacheEngine.CacheExpiry           = currentCacheRow.Cache_Expiry;
							newCacheTask.CacheEngine.CacheIsPaging         = currentCacheRow.Cache_Paging;
							newCacheTask.CacheEngine.CachePagingMaxRecords = currentCacheRow.Cache_PagingMaxRecords;
							newCacheTask.CacheEngine.CacheDBConnectionName = currentCacheRow.Cache_Connection;
							newCacheTask.CacheEngine.CacheDBActionType     = (DOCratosActionType)Enum.Parse(typeof(DOCratosActionType), currentCacheRow.Cache_ActionType);
							newCacheTask.CacheEngine.CacheUpdateOnStartup  = currentCacheRow.Cache_UpdateOnStartup;
							newCacheTask.CacheEngine.CacheExpirySeed			 = currentCacheRow.Cache_ExpirySeed; 

							// Subscribe to the relevant event(s)
							newCacheTask.CacheEngine.CacheEvent += new CratosCacheEventHandler(this.OnCacheEvent);

							// Create a new Task
							newCacheTask.Task = new Thread(new ThreadStart(newCacheTask.CacheEngine.Start));

							// Add the Engine to the Cache Task List
							this.allCacheTasks.Add(newCacheTask);
						}
						#endregion
					}
				}

				// Start all the Tasks
				foreach (CacheTask currentTask in this.allCacheTasks) { currentTask.Task.Start(); }
			}
			catch (Exception runtimeException)
			{
				// Log the error
				CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "StartCache", runtimeException, false);
			}

			// Return to calling method
			return (returnValue);
		}

		/// <summary>
		/// Stop the Cache
		/// </summary>
		/// <returns>A boolean indicating the success of the method</returns>
		public bool StopCache()
		{
			// Declare the required variables
			bool returnValue = false;

			try
			{
				// Make sure we have data to work with
				if ((this.cacheCancelToken != null) && (this.allCacheTasks != null))
				{
					// Request a cancellation
					this.cacheCancelToken.Cancel();

					// Process all the Cache Tasks
					//foreach (CacheTask currentTask in this.allCacheTasks) { currentTask.Task..Wait(5000); }
				}

				// Save the Cache DB
				returnValue = this.SaveCacheDatabase();
			}
			catch (Exception runtimeException)
			{
				// Log the error
				CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "StopCache", runtimeException, false);
			}

			// Return to calling method
			return (returnValue);
		}

		/// <summary>
		/// Load Cache Database
		/// </summary>
		/// <returns>A boolean indicating the success of the method</returns>
		public bool LoadCacheDatabase()
		{
			// Declare the required variables
			bool returnValue = false;

			try
			{
				lock (CratosCache.CacheDBLock)
				{
					// Make sure the file exists
					if (File.Exists(this.CacheFilename))
					{
						// Load the Cache File
						this.cacheDB.ReadXml(this.CacheFilename, XmlReadMode.InferSchema);

						// Validate the Version No
						if ((this.cacheDB.Tables["CacheSystem"].Rows.Count <= 0) ||
								Convert.ToInt32(this.cacheDB.Tables["CacheSystem"].Rows[0]["CacheSystem_Version"]) < CratosCache.CacheVersion)
						{
							// Clear the cache and create the relevant data
							this.cacheDB.Clear();
							this.CreateCacheVersion();
						}
					}
					else
					{
						// Create the relevant data
						this.CreateCacheVersion();
					}
				}

				// Set the successful return value
				returnValue = true;
			}
			catch (Exception runtimeException)
			{
				// Log the error
				CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "LoadCacheDatabase", runtimeException, true);
			}

			// Return to calling method
			return (returnValue);
		}

		/// <summary>
		/// Save Cache Database
		/// </summary>
		/// <returns>A boolean indicating the success of the method</returns>
		public bool SaveCacheDatabase()
		{
			// Declare the required variables
			bool returnValue = false;

			try
			{
				lock (CratosCache.CacheDBLock)
				{
					// Save the Cache
					this.cacheDB.AcceptChanges();
					this.cacheDB.WriteXml(this.CacheFilename, XmlWriteMode.WriteSchema);
				}

				// Set the successful return value
				returnValue = true;
			}
			catch (Exception runtimeException)
			{
				// Log the error
				CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "SaveCacheDatabase", runtimeException, false);
			}

			// Return to calling method
			return (returnValue);
		}

		/// <summary>
		/// Add Cache data Object
		/// </summary>
		/// <param name="newItem">New cache Item</param>
		/// <param name="cacheAssemblyElement">Cache Assembly Element</param>
		/// <returns>A boolean indicating the success of the method</returns>
		public bool AddCacheDataObject(string newItem, CacheAssemblyElement cacheAssemblyElement)
		{
			// Declare the required variables
			bool										returnValue = false;
			bool										isFound     = false;
			dsCratosCache.CacheRow	newCacheRow = null;

			try
			{
				lock (CratosCache.CacheDBLock)
				{
					// Search for the Item in the Cache
					foreach (dsCratosCache.CacheRow currentRow in this.cacheDB.Cache.Rows)
					{
						if (currentRow.Cache_DOType == newItem)
						{
							newCacheRow = currentRow;
							isFound = true;
							break;
						}
					}

					// Add the Item if required
					if (!isFound)
					{
						// Create and setup the new Cache Row
						newCacheRow              = this.cacheDB.Cache.NewCacheRow();
						newCacheRow.Cache_DOType = newItem;
						newCacheRow.Cache_Expiry = DateTime.Now;

						// Add the row to the DB
						this.cacheDB.Cache.AddCacheRow(newCacheRow);
						this.cacheDB.AcceptChanges();
					}

					// Update the relevant values
					if (!string.IsNullOrEmpty(cacheAssemblyElement.Connection))
					{
						newCacheRow.Cache_Connection = cacheAssemblyElement.Connection;
					}

					if (string.IsNullOrEmpty(cacheAssemblyElement.ActionType))
					{
						newCacheRow.Cache_ActionType = DOCratosActionType.StoredProcedure.ToString();
					}
					else
					{
						newCacheRow.Cache_ActionType = cacheAssemblyElement.ActionType;
					}

					newCacheRow.Cache_ExpirySeed = this.CacheExpirySeed;

					// Update the Cache DO with DO specific options
					foreach (CacheDataObjectElement currentDOElement in cacheAssemblyElement.CacheDataObjects)
					{
						if (currentDOElement.Name == Type.GetType(newCacheRow.Cache_DOType).FullName)
						{
							newCacheRow.Cache_UpdateOnStartup  = currentDOElement.UpdateOnStartup;
							newCacheRow.Cache_Paging           = currentDOElement.Paging;
							newCacheRow.Cache_PagingMaxRecords = currentDOElement.PagingMaxRecords;

							if (currentDOElement.ExpirySeed > 0) { newCacheRow.Cache_ExpirySeed = currentDOElement.ExpirySeed; }

							if (!string.IsNullOrEmpty(cacheAssemblyElement.Connection)) { newCacheRow.Cache_Connection = cacheAssemblyElement.Connection; }

							if (string.IsNullOrEmpty(cacheAssemblyElement.ActionType))
							{
								newCacheRow.Cache_ActionType = DOCratosActionType.StoredProcedure.ToString();
							}
							else
							{
								newCacheRow.Cache_ActionType = cacheAssemblyElement.ActionType;
							}
						}
					}

					// Accept all changes to the DB
					this.cacheDB.AcceptChanges();
				}

				// Set the successful return value
				returnValue = true;
			}
			catch (Exception runtimeException)
			{
				// Log the error
				CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "AddCacheItem", runtimeException, true);
			}

			// Return to calling method
			return (returnValue);
		}

		/// <summary>
		/// Retrieve the objects of the specified Data Type
		/// </summary>
		/// <param name="doDataType">Data Type</param>
		/// <returns>A list of objects containing the objects</returns>
		public List<IDOCratos> RetrieveItems(string doDataType)
		{
			// Declare the required variables
			List<IDOCratos> returnValue;

			// Create the relevant objects
			returnValue = new List<IDOCratos>();

			try
			{
				lock (CratosCache.CacheDBLock)
				{
					// Search for the relevant objects
					foreach (dsCratosCache.CacheRow currentCache in
																					this.cacheDB.Cache.Select(string.Format("Cache_DOType = '{0}'", doDataType)))
					{
						// Process all the Matching Items
						foreach (dsCratosCache.CacheItemsRow currentCacheItem in
																								 this.cacheDB.CacheItems.Select(string.Format("Cache_ID = {0}", currentCache.Cache_ID)))
						{
							// Instance and add the item to the list
							returnValue.Add((IDOCratos)DOCratosGlobal.BinDeserialize(currentCacheItem.Cache_Object));
						}
					}
				}
			}
			catch (Exception runtimeException)
			{
				// Log the error
				CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "RetrieveItems", runtimeException, true);
			}

			// Return to calling method
			return (returnValue);
		}

		/// <summary>
		/// Retrieve a single object of the specified Data Type
		/// </summary>
		/// <param name="doDataType">Data Type</param>
		/// <returns>A list of objects containing the objects</returns>
		public IDOCratos RetrieveSingleItem(string doDataType, Dictionary<string, object> filterCriteria)
		{
			// Declare the required variables
			IDOCratos returnValue;

			// Create the relevant objects
			returnValue = (IDOCratos)Activator.CreateInstance(Type.GetType(doDataType));

			try
			{
				lock (CratosCache.CacheDBLock)
				{
					// Search for the relevant objects
					foreach (dsCratosCache.CacheRow currentCache in
																					this.cacheDB.Cache.Select(string.Format("Cache_DOType = '{0}'", doDataType)))
					{
						// Process all the Matching Items
						foreach (dsCratosCache.CacheItemsRow currentCacheItem in
																								 this.cacheDB.CacheItems.Select(string.Format("Cache_ID = {0}", currentCache.Cache_ID)))
						{
							// Return the First Item in the list
							returnValue = ((IDOCratos)DOCratosGlobal.BinDeserialize(currentCacheItem.Cache_Object));
						}
					}
				}
			}
			catch (Exception runtimeException)
			{
				// Log the error
				CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "RetrieveSingleItem", runtimeException, true);
			}

			// Return to calling method
			return (returnValue);
		}

		#region Old Cache Methods
		///// <summary>
		///// Refresh the Cache
		///// </summary>
		///// <returns>A boolean value is returned indicating the success of the method</returns>
		//public virtual void RefreshCache()
		//{
		//  // Declare the required variables

		//  // Determine if the Worker Thread is busy
		//  if (!this.m_Worker.IsBusy)
		//  {
		//    // Fire the relevant event if required
		//    if (this.CacheRefreshStarted != null) { this.CacheRefreshStarted(this, null); }

		//    // Start the Worker Thread
		//    this.m_Worker.RunWorkerAsync();
		//  }

		//  // Return to calling method
		//  return;
		//}

		///// <summary>
		///// Clear Cache
		///// </summary>
		///// <returns>A boolean value is returned indicating the success of the method</returns>
		//public virtual bool ClearCache()
		//{
		//  // Declare the required variables
		//  bool returnValue = false;

		//  try
		//  {
		//    // Make sure a refresh is not currently running
		//    if (!this.m_Worker.IsBusy)
		//    {
		//      // Clear the Cache
		//      this.cacheDB.Clear();

		//      // Save the Cache
		//      this.cacheDB.WriteXml(this.CacheFilename, XmlWriteMode.WriteSchema);

		//      // Set the successful return value
		//      returnValue = this.LoadCache();
		//    }
		//  }
		//  catch (Exception runtimeException)
		//  {
		//    // Log the error
		//    CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "ClearCache", runtimeException, true);
		//  }

		//  // Return to calling method
		//  return (returnValue);
		//}

		#endregion
		#endregion

		#region Class Support Methods
		private void UpdateCacheExpiry(string cacheDOType, DateTime cacheExpiry)
		{
			// Declare the required variables

			try
			{
				lock (CratosCache.CacheDBLock)
				{
					// Process all the items in the cache
					foreach (dsCratosCache.CacheRow currentRow in this.cacheDB.Cache.Rows)
					{
						// Validate the Cache Type
						if (currentRow.Cache_DOType == cacheDOType)
						{
							// Update the Cache Item Expiry
							currentRow.Cache_Expiry = cacheExpiry;
							break;
						}
					}

					// Accept all the changes to the Dataset
					this.cacheDB.AcceptChanges();
				}
			}
			catch (Exception runtimeException)
			{
				// Log the error
				CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "UpdateCacheExpiry", runtimeException, true);
			}

			// Return to calling method
			return;
		}

		private void CreateCacheVersion()
		{
			// Declare the required variables
			dsCratosCache.CacheSystemRow newSystemRow;

			try
			{
				// Create and setup the new System row
				newSystemRow											= this.cacheDB.CacheSystem.NewCacheSystemRow();
				newSystemRow.CacheSystem_Version	= CratosCache.CacheVersion;

				// Add the row and accept all changes
				this.cacheDB.CacheSystem.AddCacheSystemRow(newSystemRow);
				this.cacheDB.AcceptChanges();
			}
			catch (Exception runtimeException)
			{
				// Log the error
				CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", "CreateCacheVersion", runtimeException, true);
			}

			// Return to calling method
			return;
		}
		#endregion

		#region Custom Event handlers
		private void OnCacheEvent(CratosCacheEngine cacheEngine, CratosCacheAction cacheAction, string eventMsg)
		{
			// Declare the required variables

			// Handle the Cache Event Action
			switch (cacheAction)
			{
				case CratosCacheAction.Exception:
					// Log the error
					CratosHelper.LogMessage(CratosLogSeverity.Exception, "Cratos Caching", 
																	string.Format("{0} - {1}", cacheEngine.Name, eventMsg), null, false);
					break;

				case CratosCacheAction.Informational:
					// Log the informational message
					CratosHelper.LogMessage(CratosLogSeverity.Informational, "Cratos Caching",
																	string.Format("{0} - {1}", cacheEngine.Name, eventMsg), null, false);
					break;

				case CratosCacheAction.SaveDatabase:
					// Log the informational message
					CratosHelper.LogMessage(CratosLogSeverity.Informational, "Cratos Caching",
																	string.Format("{0} - Saving Cache DB", cacheEngine.Name), null, false);

					// Save the Cache Database
					this.SaveCacheDatabase();
					break;

				case CratosCacheAction.LoadSuccess:
					// Log the informational message
					CratosHelper.LogMessage(CratosLogSeverity.Informational, "Cratos Caching",
																	string.Format("{0} - Updating the Cache Expiry", cacheEngine.Name), null, false);

					// Update the Expiry and Save the Cache Database
					this.UpdateCacheExpiry(cacheEngine.CacheDOType.AssemblyQualifiedName, cacheEngine.CacheExpiry);
					this.SaveCacheDatabase();
					break;
			}

			// Return to calling method
			return;
		}
		#endregion
	}
}
